
/* GCSx
** SCRIPTEDIT.CPP
**
** Script support
** Expands basic Script to include editor functionality
*/

/*****************************************************************************
** Copyright (C) 2003-2006 Janson
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
*****************************************************************************/

#include "all.h"

ScriptEdit::ScriptEdit(WorldEdit* myWorld, ScriptType type, int myId) : Script(myWorld, type, myId) { start_func
    if (myId) assert(myWorld);

    // Find an unused "Untitled" name
    if (myId) {
        int pos = 1;
        string findName;

        if (type == SCRIPT_NOTE) {
            findName = "notes";
            while (myWorld->findScriptNote(findName)) {
                findName = "notes ";
                findName += intToStr(++pos);
            }
        }
        else if (type == SCRIPT_CODE) {
            findName = "script";
            while (myWorld->findScriptCode(findName)) {
                findName = "script ";
                findName += intToStr(++pos);
            }
        }
        else {
            assert(type == SCRIPT_LIBRARY);
            findName = "library";
            while (myWorld->findScriptLib(findName)) {
                findName = "library ";
                findName += intToStr(++pos);
            }
        }
    
        findName[0] = toupper(findName[0]);
        name = findName;
        headerModified = contentModified = 1;
    }
    else {
        assert((type == SCRIPT_NOTE) || (type == SCRIPT_CODE) || (type == SCRIPT_LIBRARY));
        headerModified = contentModified = 0;
    }
    
    nameL = name;
    toLower(nameL);

    browserNode = NULL;
    
    dependencyModified = sourceModified = 0;
    
    inMiddleOfUndoBlock = 0;
    disassociated = 0;
    desktop->broadcastObjChange(OBJ_SCRIPT | OBJMOD_CREATE, this, 0, 0);
}

ScriptEdit::~ScriptEdit() { start_func
    // @TODO: doesn't appear to be needed; no way to delete accidentally
    // if (!disassociated) desktop->broadcastObjChange(OBJ_SCRIPT | OBJMOD_DELETE, this, 0, 0);

    // World should be the one deleting us, which will delete our node also
}

void ScriptEdit::setInfo(WorldEdit* myWorld, int newId) { start_func
    world = myWorld;
    id = newId;
}

void ScriptEdit::compile() { start_func
    assert(lockCount);

    cleanForCompile();
    Script::compile();
    if (!compileErrors) {
        // @TODO: mark all dependants as dependencyModified
        dependencyModified = 0;
        sourceModified = 0;
    }
    setHeaderModified();
    setContentModified();
}

void ScriptEdit::parseFuncs() { start_func
    assert(lockCount);

    cleanForCompile();
    Script::parseFuncs();
    setHeaderModified();
    setContentModified();
}

void ScriptEdit::clean(int full) { start_func
    assert(!cached);

    // Full = everything
    // otherwise- leave function parse results
    
    if ((full) || (memStatus == CODE_UNCOMPILED)) {
        debugWrite(DEBUG_COMPILE, "Cleaning '%s' for recompile... (full)", name.c_str());
        if (functions) {
            destroyFunctionMap(functions);
            delete functions;
            functions = NULL;
        }
        
        if ((memStatus != CODE_UNCOMPILED) || (diskStatus != CODE_UNCOMPILED)) {
            setHeaderModified();
            setContentModified();
        }
        
        memStatus = CODE_UNCOMPILED;
        diskStatus = CODE_UNCOMPILED;
    }
    else {
        debugWrite(DEBUG_COMPILE, "Cleaning '%s' for recompile... (partial)", name.c_str());

        if ((memStatus != CODE_PARSED) || (diskStatus != CODE_PARSED)) {
            setHeaderModified();
            setContentModified();
        }
        
        memStatus = CODE_PARSED;
        if (diskStatus > CODE_PARSED) diskStatus = CODE_PARSED;
    }
    
    delete links;
    links = NULL;
    delete[] bytecode;
    bytecode = NULL;
    compileErrors = 0;
    compileWarnings = 0;
}

void ScriptEdit::cleanForCompile(int force) { start_func
    // @TODO: check all dependencies first, here
    
    // Cleans whatever needs to be cleaned based on modified/etc
    // in preparation for a compile
    if ((sourceModified) || (compileErrors) || (force) || (memStatus == CODE_UNCOMPILED)) {
        clean(1);
    }
    else if ((dependencyModified) || (memStatus == CODE_PARSED)) {
        clean(0);
    }
    // (always set up for relink or not linked when saving)
    else if (memStatus == CODE_LINKED) {
        // Don't need to touch diskStatus as it should NEVER be CODE_LINKED
        assert(diskStatus != CODE_LINKED);
        memStatus = CODE_COMPILED;
    }
}

void ScriptEdit::link() { start_func
    doLink();
}

void ScriptEdit::decompile() { start_func
    assert(lockCount);
    
    cleanForCompile();
    compile();
    if (bytecode) decompileBytecode(bytecode, links);
}

void ScriptEdit::disassociate() throw_File { start_func
    // Ensure not cached- our exception point
    cacheLoad();
    
    // Drop browser node
    dropBrowser();
    
    // Notify objects
    desktop->broadcastObjChange(OBJ_SCRIPT | OBJMOD_DELETE, this, 0, 0);
    
    disassociated = 1;
}

void ScriptEdit::reassociate(TreeView* node) { start_func
    assert(disassociated);
    
    // Notify objects
    desktop->broadcastObjChange(OBJ_SCRIPT | OBJMOD_CREATE, this, 0, 0);
    
    // Browser node
    addToBrowser(node, 0);

    // Ensure written to file
    headerModified = contentModified = 1;
    disassociated = 0;
}

void ScriptEdit::setHeaderModified() { start_func
    if (!headerModified) headerModified = 1;
}

void ScriptEdit::setContentModified() { start_func
    if (!contentModified) {
        contentModified = 1;
        // No possibility of returning to cache
        delete cacheFile;
        cacheFile = NULL;
    }
}

void ScriptEdit::codeModifiedPre(ContentChangeType type, int firstRow, int numRows, EditBox* editbox, Window* srcWin) { start_func
    if (world) {
        if ((!inMiddleOfUndoBlock) && (type != SCRIPT_MODIFY_DONE)) {
            getWorldEdit()->undo.preUndoBlock();
            inMiddleOfUndoBlock = 1;
        }
    
        if (type == SCRIPT_MODIFY_LINE) {
            getWorldEdit()->undo.storeUndoScriptModify(id, source, firstRow, numRows, editbox, srcWin);
        }
        else if (type == SCRIPT_INSERT_LINES) {
            getWorldEdit()->undo.storeUndoScriptInsert(id, firstRow, numRows, editbox, srcWin);
        }
        else if (type == SCRIPT_REMOVE_LINES) {
            getWorldEdit()->undo.storeUndoScriptRemove(id, source, firstRow, numRows, editbox, srcWin);
        }
        else if ((type == SCRIPT_MODIFY_DONE) && (inMiddleOfUndoBlock)) {
            getWorldEdit()->undo.postUndoBlock();
            inMiddleOfUndoBlock = 0;
        }
    }
}

void ScriptEdit::codeModifiedPost(ContentChangeType type, int firstRow, int numRows, EditBox* editbox, Window* exWin) { start_func
    setHeaderModified();
    setContentModified();
    getWorldEdit()->setGlobalLinksModified();
    sourceModified = 1;

    if (type == SCRIPT_MODIFY_LINE) {
        desktop->broadcastObjChange(OBJ_SCRIPT | OBJMOD_LINE, this, firstRow, numRows, exWin);
    }
    else if (type == SCRIPT_INSERT_LINES) {
        desktop->broadcastObjChange(OBJ_SCRIPT | OBJMOD_INSERT, this, firstRow, numRows, exWin);
    }
    else if (type == SCRIPT_REMOVE_LINES) {
        desktop->broadcastObjChange(OBJ_SCRIPT | OBJMOD_REMOVE, this, firstRow, numRows, exWin);
    }
}

void ScriptEdit::dropBrowser() { start_func
    browserNode = NULL;
}

void ScriptEdit::addToBrowser(TreeView* node, int makeCurrent) { start_func
    browserNode = new TreeView(name, this, 0, treeviewEventWrap);
    browserNode->setIcon(scriptType == SCRIPT_NOTE ? 8 : (scriptType == SCRIPT_LIBRARY ? 16 : (defaultId ? 13 : 12)));
    node->insert(browserNode, makeCurrent);
}

int ScriptEdit::treeviewEvent(int code, int command, int check) { start_func
    if (check) {
        if (command == EDIT_DELETE) return Window::COMMAND_ENABLE;
        return Window::COMMAND_HIDE;
    }

    switch (command) {
        case LV_SELECT:
            openEditWindow();
            return 1;
            
        case LV_RCLICK:
            // @TODO: Should actually be a popup with this as an item
            propertiesDialog(desktop->findPreviousFocusWindow());
            return 1;
        
        case EDIT_DELETE:
        case LV_DELETE:
            // Handles any error conditions/cancellation for us
            assert(world);
            getWorldEdit()->deleteScript(this, desktop->findPreviousFocusWindow());
            return 1;
    }
    
    return 0;
}

int ScriptEdit::treeviewEventWrap(void* ptr, int code, int command, int check) { start_func
    return ((ScriptEdit*)ptr)->treeviewEvent(code, command, check);
}

void ScriptEdit::setName(const string& newName, Window* srcWin, Window* exWin) throw_Undo { start_func
    if (name != newName) {
        setHeaderModified();
        if (world) {
            getWorldEdit()->undo.storeUndoName(UndoBuffer::UNDO_SCRIPTNAME, id, name, newName, srcWin);
            world->deindexScript(this);
        }
        name = newName;
        nameL = newName;
        toLower(nameL);
        if (world) world->indexScript(this);
        if (browserNode) browserNode->changeName(newName);
    
        desktop->broadcastObjChange(OBJ_SCRIPT | OBJMOD_NAME, this, 0, 0, exWin);
    }
}

void ScriptEdit::setDefault(AnimGroup* newAnimgroup, TileSet* newTileset, int newId, Window* srcWin, Window* exWin) throw_Undo { start_func
    if ((newAnimgroup != defaultAnimgroup) || (newTileset != defaultTileset) || (newId != defaultId)) {
        assert(newId >= 0);
        if ((newAnimgroup) || (newTileset)) {
            assert(newId);
            if (newAnimgroup) assert(!newTileset);
            else assert(!newAnimgroup);
        }
        else assert(!newId);
        
        setHeaderModified();
        int aId = 0;
        int tId = 0;
        if (defaultAnimgroup) aId = defaultAnimgroup->getId();
        if (defaultTileset) tId = defaultTileset->getId();
        if (world) getWorldEdit()->undo.storeUndoScriptDefault(id, aId, tId, defaultId, srcWin);
        defaultAnimgroup = newAnimgroup;
        defaultTileset = newTileset;
        defaultId = newId;
        if (browserNode) browserNode->setIcon(scriptType == SCRIPT_NOTE ? 8 : (scriptType == SCRIPT_LIBRARY ? 16 : (defaultId ? 13 : 12)));
    
        desktop->broadcastObjChange(OBJ_SCRIPT | OBJMOD_DEFAULT, this, 0, 0, exWin);
    }
}

list<string>& ScriptEdit::getSource() { start_func
    return source;
}

void ScriptEdit::openEditWindow() { start_func
    try {
        ScriptEditor* editor = new ScriptEditor(&source, this); // Our exception point
        editor->runWindowed();
    }
    catch (FileException& e) {
        guiErrorBox(string(e.details), errorTitleFile);
    }
}

int ScriptEdit::propertiesDialog(Window* srcWin, Window* exWin) { start_func
    string newName = name;
    AnimGroup* newAnimgroup = defaultAnimgroup;
    TileSet* newTileset = defaultTileset;
    int newId = defaultId;
    
    int result;
    if (scriptType == SCRIPT_NOTE) {
        result = ScriptPropertiesDialog::createNotes()->run(&newName, NULL, NULL, NULL, this, getWorldEdit());
    }
    else if (scriptType == SCRIPT_LIBRARY) {
        result = ScriptPropertiesDialog::createLibrary()->run(&newName, NULL, NULL, NULL, this, getWorldEdit());
    }
    else {
        result = ScriptPropertiesDialog::createScript()->run(&newName, &newAnimgroup, &newTileset, &newId, this, getWorldEdit());
    }

    if (result) {
        try {
            markLock();
        }
        catch (FileException& e) {
            guiErrorBox(string(e.details), errorTitleFile);
            return 0;
        }
        try {
            if (world) getWorldEdit()->undo.preUndoBlock();
            setName(newName, srcWin, exWin);
            if (scriptType == SCRIPT_CODE) setDefault(newAnimgroup, newTileset, newId, srcWin, exWin);
            if (world) getWorldEdit()->undo.postUndoBlock();
        }
        catch (UndoException& e) {
            markUnlock();
            return 0;
        }
        markUnlock();
        return 1;
    }
    
    return 0;
}

int ScriptEdit::isHeaderModified() const { start_func
    return headerModified;
}

int ScriptEdit::isContentModified() const { start_func
    return contentModified;
}

Uint32 ScriptEdit::saveHeader(FileWrite* file) throw_File { start_func
    assert(world);
    assert(id);

    // clean if needed to based on modifieds- don't want to save
    // unclean data (this fixes memStatus)
    if (contentModified) {
        assert(!cached);
        cleanForCompile();
    }
    
    file->writeInt(id);
    file->writeStr(name);
    // @TODO: option to not save compilation results
    file->writeInt(contentModified ? memStatus : diskStatus);
    // @TODO: option to not save source
    file->writeInt(1);
    file->writeInt(scriptType);
    if (defaultAnimgroup) file->writeInt(defaultAnimgroup->getId());
    else file->writeInt(0);
    if (defaultTileset) file->writeInt(defaultTileset->getId());
    else file->writeInt(0);
    file->writeInt(defaultId);

    // @TODO: option to not save compilation results
    if (memStatus >= CODE_PARSED) {
        assert(functions);
        
        file->writeInt(functions->size());
        
        FunctionMap::iterator pos;
        FunctionMap::iterator end = functions->end();
        for (pos = functions->begin(); pos != end; ++pos) {
            string name = (*pos).first;
            file->writeStr(name);
            
            Function& func = (*pos).second;
            file->writeInt(func.numparam);
            if (func.numparam) {
                for (int param = 0; param < func.numparam; ++param) {
                    file->writeInt(func.parameters[param].baseType);
                    file->writeInt(func.parameters[param].subType);
                    file->writeInt(func.parameters[param].flags);
                }
            }
            file->writeInt(func.returntype.baseType);
            file->writeInt(func.returntype.subType);
            file->writeInt(func.returntype.flags);
            file->writeInt(func.offset);
        }
    }
    
    return 1;
}

void ScriptEdit::saveContent(FileWrite* file) throw_File { start_func
    assert(world);
    assert(id);
    
    // clean if needed to based on modifieds- don't want to save
    // unclean data
    cleanForCompile();
    
    // @TODO: option to not save source
    file->writeInt(source.size());
    
    list<string>::iterator pos;
    list<string>::iterator end = source.end();
    for (pos = source.begin(); pos != end; ++pos) {
        file->writeStr(*pos);
    }
            
    // @TODO: option to not save compilation results
    if (memStatus >= CODE_COMPILED) {
        assert(bytecode);
        assert(links);
        
        file->writeIntBulk(bytecode, bytecode[0] + 2);
        
        file->writeInt(links->size());
        list<LinkEntry>::iterator pos;
        list<LinkEntry>::iterator end = links->end();
        for (pos = links->begin(); pos != end; ++pos) {
            LinkEntry& link = *pos;
            file->writeInt(link.type);
            file->writeInt(link.offset);
            file->writeStr(link.scope);
            file->writeStr(link.name);
            file->writeStr(link.wart);
        }
    }
}

void ScriptEdit::saveSuccess() { start_func
    headerModified = 0;
    contentModified = 0;
    diskStatus = memStatus;
}

void ScriptEdit::cachedContent(FileRead* file, int oldData) { start_func
    // If already cached..
    if (cached) {
        delete cacheFile;
        cacheFile = file;
    }
    else if (oldData) {
        delete file;
    }
    else {
        // Cache ourselves?
        if (!lockCount) {
            delete cacheFile;
            cacheFile = file;
            source.clear();
            delete[] bytecode;
            delete links;
            bytecode = NULL;
            links = NULL;
            cached = 1;
        }
        else {
            delete file;
        }
    }
}

